package com.liebaut.util.jackson;

import com.fasterxml.jackson.core.io.SerializedString;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.std.EnumDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.ser.std.EnumSerializer;
import com.fasterxml.jackson.databind.util.ClassUtil;
import com.fasterxml.jackson.databind.util.EnumResolver;
import com.fasterxml.jackson.databind.util.EnumValues;

import javax.xml.bind.annotation.XmlEnumValue;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class EnumModule extends SimpleModule {


    public static String enumName(Enum en) {
        Class enumClass = en.getClass();
        try {
            XmlEnumValue xmlEnumValue = enumClass.getField(en.name()).getAnnotation(XmlEnumValue.class);
            return xmlEnumValue != null ? xmlEnumValue.value() : en.name().toLowerCase();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static <T> T valueOf(Class clazz, String name) {
        for(Object e : clazz.getEnumConstants()) {
            String tmp = enumName((Enum) e);
            if (name.equals(tmp))
                return (T) e;
        }
        throw new IllegalArgumentException("Invalid enum value: " + name);
    }

    @Override
    public void setupModule(SetupContext context) {
        context.addBeanSerializerModifier(new BeanSerializerModifier() {
            public JsonSerializer<?> modifyEnumSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc, JsonSerializer<?> serializer) {
                Class<Enum<?>> enumClass = (Class<Enum<?>>) type.getRawClass();
                return new EnumSerializer(constructEnumValues(enumClass), false);
            }
        });

        context.addBeanDeserializerModifier(new BeanDeserializerModifier() {
            public JsonDeserializer<?> modifyEnumDeserializer(DeserializationConfig config, JavaType type, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
                Class<Enum<?>> enumClass = (Class<Enum<?>>) type.getRawClass();
                return new EnumDeserializer(constructEnumResolver(enumClass));
            }
        });
    }

    private static EnumResolver constructEnumResolver(Class<Enum<?>> enumClass) {
        Enum<?>[] enumValues = enumClass.getEnumConstants();
        HashMap<String, Enum<?>> map = new HashMap<String, Enum<?>>();
        // from last to first, so that in case of duplicate values, first wins
        for (int i = enumValues.length; --i >= 0; ) {
            Enum<?> e = enumValues[i];
            map.put(enumName(e), e);
        }
        try {
            return newEnumResolver(enumClass, enumValues, map);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static EnumResolver newEnumResolver(Class enumClass, Enum[] enumValues, HashMap map) throws Exception {
        Constructor<EnumResolver> constructor = EnumResolver.class.getDeclaredConstructor(new Class[]{Class.class, Enum[].class, HashMap.class});
        constructor.setAccessible(true);
        return constructor.newInstance(enumClass, enumValues, map);
    }

    private static EnumValues constructEnumValues(Class<Enum<?>> enumClass) {
        Class<? extends Enum<?>> cls = ClassUtil.findEnumType(enumClass);
        Enum<?>[] values = cls.getEnumConstants();
        if (values != null) {
            try {
                Map<Enum<?>, SerializedString> map = new HashMap<Enum<?>, SerializedString>();
                for (Enum<?> en : values) {
                    map.put(en, new SerializedString(enumName(en)));
                }
                return newEnumValues(enumClass, map);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        throw new IllegalArgumentException("Can not determine enum constants for Class " + enumClass.getName());
    }

    private static EnumValues newEnumValues(Class enumClass, Map map) throws Exception {
        Constructor<EnumValues> constructor = EnumValues.class.getDeclaredConstructor(new Class[]{Class.class, Map.class});
        constructor.setAccessible(true);
        return constructor.newInstance(new Object[]{enumClass, map});
    }
}
